﻿/*
 * imaeg - generic image utility in C#
 * Copyright (C) 2010  ed <tripflag@gmail.com>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License v2
 * (version 2) as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, refer to the following URL:
 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt
 */
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Drawing.Imaging;

namespace imaeg {
    public partial class Form1 : Form {
        public Form1() {
            InitializeComponent();
        }

        /*
         * ======== A LITTLE NOTE FROM THE DEV ========
         * 
         * *-Currently rewriting stuff to be more OOP-*
         * 
         *    Instead of doing everything at once, I
         *   plan to do a little bit at a time. As of
         *    now the code to load and filter images
         *   should be somewhat decent.
         * 
         *  As nothing seems to have broken, I'll just
         *  go ahead and post this as v1.0.1 fuck yes.
         * 
         *                                      -ed
         */



        ImgMan man;             // The Man
        int cur;                // Current image
        string mvPre;           // Base folder for moving
        string path;            // Application path
        string initFile;        // Initial image to show
        bool folderInit;        // Have we made organizing folders yet?
        Rectangle rect;         // Widnow size memory (kiosk mode)
        Rectangle[] zoom;       // The zoom level memory
        bool lockEdges = true;  // When moving, lock onto screen edges?
        int scrollIdent;        // Sticky keys scrolling filter
        long slideLast;         // Slideshow, last tick
        long slideDelay;        // Slideshow, tickdelay
        bool slideshow;         // Slideshow?
        Point chrome;           // Windowborder offset

        private void Form1_Load(object sender, EventArgs e) {
            this.Text += " v" + Application.ProductVersion;
            this.Show(); Application.DoEvents();
            zoom = new Rectangle[10];
            rect = Rectangle.Empty;
            chrome = Point.Empty;
            initFile = null;
            scrollIdent = 0;
            cur = 0;

            // Crude hack to start in fullscreen
            dsp.BackgroundImageLayout = ImageLayout.Tile;
            Form1_KeyDown(sender, new KeyEventArgs(Keys.F11));

            // Launched with an argument
            if (Program.args.Length > 0) {
                path = Program.args[0];
                if (System.IO.File.Exists(path)) {
                    initFile = path;
                    path = path.Substring(0,
                        path.LastIndexOf("\\"));
                }
                prepare(path);

            } else {
                // naw dude
                fileBrowser();
            }
            
            // Apply zooming with scroll wheel
            this.MouseWheel += new MouseEventHandler(dsp_MouseWheel);

            // Sliteshow
            Timer slideshowTimer = new Timer();
            slideshowTimer.Tick += new EventHandler(slideshowTimer_Tick);
            slideshowTimer.Interval = 10;
            slideshowTimer.Start();

            Program.dbg("Program initiated");
        }

        void slideshowTimer_Tick(object sender, EventArgs e) {
            if (!slideshow) return;
            long tick = System.DateTime.Now.Ticks / 10000;
            if (tick > slideLast + slideDelay) {
                Program.dbg("Slideshow changing image");
                slideLast = tick;
                nextPic(1);
            }
        }

        private void Form1_DragOver(object sender, DragEventArgs e) {
            e.Effect = DragDropEffects.All;
        }

        private void Form1_DragDrop(object sender, DragEventArgs e) {
            prepare(((string[])e.Data.GetData(DataFormats.FileDrop))[0]);
        }

        private void dsp_Click(object sender, EventArgs e) {
            // This interferes with zooman, fuck it
            //fileBrowser();
        }

        void fileBrowser() {
            OpenFileDialog ofd = new OpenFileDialog();
            ofd.ShowDialog(); string fn = ofd.FileName;
            if (string.IsNullOrEmpty(fn)) return;
            fn = fn.Substring(0, fn.LastIndexOf("\\"));
            prepare(fn);
        }

        // Initialize application to given path
        void prepare(string path) {
            if (folderInit) {
                Program.dbg("Clearing empty folders ...");
                // App has already been prepared; method called
                // to delete empty/dummy organiizng folders
                for (int a = 0; a < 10; a++) {
                    try {
                        System.IO.Directory.Delete(mvPre + a);
                    } catch { }
                }
            }
            if (string.IsNullOrEmpty(path)) return;
            Program.dbg("Loading file/folder " + path);
            cur = 0;
            path += "\\";
            this.path = path;
            mvPre = path + "outdir";

            List<string> files = new List<string>(
                System.IO.Directory.GetFiles(path));
            for (int a = 0; a < files.Count; a++) {
                files[a] = files[a].Substring(path.Length);
                string ext = files[a].Substring(files[a].
                    LastIndexOf(".") + 1).ToLower();

                // Filter to images natively supported by .NET
                if (ext != "jpg" && ext != "png" &&
                    ext != "gif" && ext != "bmp" &&
                    ext != "emf" && ext != "ico" &&
                    ext != "wmf" && ext != "tiff" &&
                    ext != "jpeg") {
                    files.RemoveAt(a);
                    a--;
                }
            }
            Program.dbg("Found " + files.Count + " images");
            files.Sort();
            man = new ImgMan(path, files.ToArray());

            // Are we to display a specific initial file?
            if (!string.IsNullOrEmpty(initFile)) {
                Program.dbg("Navigating to image " + initFile);
                initFile = initFile.Substring(path.Length);
                for (int a = 0; a < files.Count; a++)
                    if (initFile == files[a])
                        cur = a;
            }
            nextPic(0);
        }

        private void Form1_KeyDown(object sender, KeyEventArgs e) {
            //this.Text = e.KeyCode.ToString();
            Program.dbg("Keystroke " +
                (e.Shift ? "1" : "0") +
                (e.Control ? "1" : "0") +
                (e.Alt ? "1" : "0") +
                "(" + (int)e.KeyCode + ") " +
                e.KeyCode.ToString());

            // Rotation
            if (e.KeyCode == Keys.OemOpenBrackets) {
                man.filter.incRotate(-1);
                nextPic(0);
            }
            if (e.KeyCode == Keys.OemCloseBrackets) {
                man.filter.incRotate(1);
                nextPic(0);
            }
            if (e.KeyCode == Keys.R) {
                if (e.Shift) man.filter.incRotate(-1);
                else man.filter.incRotate(1);
                nextPic(0);
            }

            // Exit
            if (e.KeyCode == Keys.Escape)
                Application.Exit();

            // Help // About
            if (e.KeyCode == Keys.F1) if (DialogResult.OK == MessageBox.Show(
                "imaeg v" + Application.ProductVersion + " by ed <tripflag@gmail.com>\r\n\r\n" +
                "powered by #/G/TV ENTERPRISES, ltd.\r\n\r\n" +
                "Visit website?", "sup", MessageBoxButtons.OKCancel))
                    System.Diagnostics.Process.Start(
                        "http://code.google.com/p/imaeg/");

            if (!e.Alt && !e.Control && !e.Shift && (
                e.KeyCode == Keys.Left ||
                e.KeyCode == Keys.PageUp ||
                e.KeyCode == Keys.Up)) nextPic(-1);

            if (!e.Alt && !e.Control && !e.Shift && (
                e.KeyCode == Keys.Right ||
                e.KeyCode == Keys.PageDown ||
                e.KeyCode == Keys.Down ||
                e.KeyCode == Keys.Space)) nextPic(1);

            if (e.KeyCode >= Keys.NumPad0 &&
                e.KeyCode <= Keys.NumPad9) {
                if (e.Alt) {

                    // Bright/cont/gamma ctl
                    if (e.KeyCode == Keys.NumPad1) man.filter.brigh -= 0.03f;
                    if (e.KeyCode == Keys.NumPad2) man.filter.contr *= 11 / 12.0;
                    if (e.KeyCode == Keys.NumPad3) man.filter.gamma /= 11 / 12.0;
                    if (e.KeyCode == Keys.NumPad4) man.filter.brigh = 9001;
                    if (e.KeyCode == Keys.NumPad5) man.filter.contr = 9001;
                    if (e.KeyCode == Keys.NumPad6) man.filter.gamma = 9001;
                    if (e.KeyCode == Keys.NumPad7) man.filter.brigh += 0.03f;
                    if (e.KeyCode == Keys.NumPad8) man.filter.contr /= 11 / 12.0;
                    if (e.KeyCode == Keys.NumPad9) man.filter.gamma *= 11 / 12.0;

                } else {

                    // Organization modo
                    int k = (int)e.KeyCode;
                    k -= (int)Keys.NumPad0;
                    if (!folderInit) {
                        if (DialogResult.Yes != MessageBox.Show(
                            "Enable organization modo?", "Faggotry",
                            MessageBoxButtons.YesNo)) return;
                        for (int a = 0; a < 10; a++) System.IO.
                            Directory.CreateDirectory(mvPre + a);
                        folderInit = true;
                    }
                    dsp.Image.Dispose();
                    dsp.Image = null;
                    string name = man.getName(cur);
                    System.IO.File.Move(path + name,
                        mvPre + k + "\\" + name);
                    man.remove(cur);
                }
                nextPic(0);
            }
            if (e.KeyCode == Keys.F11 ||
                e.KeyCode == Keys.Enter && e.Alt) {
                Program.dbg("Toggling fullscreen");
                if (this.rect == Rectangle.Empty) {
                    this.FormBorderStyle = FormBorderStyle.None;
                    this.WindowState = FormWindowState.Normal;
                    Application.DoEvents();
                    this.rect = this.Bounds;
                    this.Bounds = Screen.FromControl(this).Bounds;
                } else {
                    this.FormBorderStyle = FormBorderStyle.Sizable;
                    this.WindowState = FormWindowState.Normal;
                    Application.DoEvents();
                    this.Bounds = this.rect;
                    this.rect = Rectangle.Empty;
                }
            }
            if (e.KeyCode == Keys.I) {
                stat.Visible = !stat.Visible;
            }
            if (e.KeyCode == Keys.F2) {
                string ofn, fn;
                ofn = fn = man.getName(cur);
                string ext = fn.Substring(fn.LastIndexOf("."));
                fn = fn.Substring(0, fn.Length - ext.Length);
                string str = InputBox.Derp("Enter new filename", fn);
                if (str == null) return;
                fn = str + ext;
                if (ofn == fn) return;
                dsp.Image.Dispose();
                dsp.Image = null;
                stat.Text = "Moving...";
                Application.DoEvents();
                System.IO.File.Move(
                    path + ofn,
                    path + fn);
                man.setName(cur, fn);
                nextPic(e.Alt ? 1 : 0);
            }
            if (e.KeyCode == Keys.F9) {
                man.filter.guiTweek();
                nextPic(0);
            }
            if (e.KeyCode == Keys.Home) {
                recenter(); //see alt-4
            }
            if (e.KeyCode == Keys.Up) {
                if (e.Control) scrollSmooth(dsp.Left, dsp.Top + span.Height); //dsp.Top += span.Height;
                if (e.Shift) scrollSmooth(dsp.Left, dsp.Top + span.Height / 2); //dsp.Top += span.Height / 2;
                validateBounds();
            }
            if (e.KeyCode == Keys.Down) {
                if (e.Control) scrollSmooth(dsp.Left, dsp.Top - span.Height); //dsp.Top -= span.Height;
                if (e.Shift) scrollSmooth(dsp.Left, dsp.Top - span.Height / 2); //dsp.Top -= span.Height / 2;
                validateBounds();
            }
            if (e.KeyCode == Keys.Left) {
                if (e.Control) scrollSmooth(dsp.Left + span.Width, dsp.Top); //dsp.Left += span.Width;
                if (e.Shift) scrollSmooth(dsp.Left + span.Width / 2, dsp.Top); //dsp.Left += span.Width / 2;
                validateBounds();
            }
            if (e.KeyCode == Keys.Right) {
                if (e.Control) scrollSmooth(dsp.Left - span.Width, dsp.Top); //dsp.Left -= span.Width;
                if (e.Shift) scrollSmooth(dsp.Left - span.Width / 2, dsp.Top); //dsp.Left -= span.Width / 2;
                validateBounds();
            }
            if (e.KeyCode == Keys.B) {
                dsp.BackgroundImage = null;
                this.BackColor = Color.Black;
                if (e.Control) this.BackColor = Color.White;
                if (e.Shift) dsp.BackgroundImage = new Bitmap(embedded("transparent.png"));
                if (e.Alt) using (ColorDialog cd = new ColorDialog()) {
                        cd.ShowDialog();
                        this.BackColor = cd.Color;
                    }
            }
            if (e.KeyCode == Keys.L) {
                lockEdges = !lockEdges;
            }
            if (e.KeyCode >= Keys.D0 &&
                e.KeyCode <= Keys.D9) {
                if (e.Shift) {

                    // Save this camera angle
                    zoom[e.KeyCode - Keys.D0] = dsp.Bounds;

                } else if (e.Alt) {

                    // Zoom to specific prefix
                    if (e.KeyCode == Keys.D1) {
                        // Scale 1:1
                        dsp.Bounds = new Rectangle(Point.Empty, dsp.Image.Size);
                    }
                    if (e.KeyCode == Keys.D2) {
                        // Scale to Width
                        double frac = span.Width / (1.0 * dsp.Image.Width);
                        int height = (int)(dsp.Image.Height * frac);
                        dsp.Bounds = new Rectangle(Point.Empty, new Size(span.Width, height));
                    }
                    if (e.KeyCode == Keys.D3) {
                        // Scale to Height
                        double frac = span.Height / (1.0 * dsp.Image.Height);
                        int width = (int)(dsp.Image.Width * frac);
                        dsp.Bounds = new Rectangle(Point.Empty, new Size(width, span.Height));
                    }
                    if (e.KeyCode == Keys.D4) {
                        // Scale to screen (reduntant, "home")
                        recenter();
                    }
                } else {
                    try {
                        // Recall camera angle (if any)
                        dsp.Bounds = zoom[e.KeyCode - Keys.D0];
                    } catch { }
                }
            }
            if (e.KeyCode == Keys.F12) {
                if (e.Control) {
                    try {
                        slideDelay = (long)(Convert.ToDouble(InputBox.Derp(
                            "Slideshow delay (seconds, float)",
                            "" + (slideDelay / 1000.0))) * 1000);
                    } catch {
                        MessageBox.Show("THE FUCK YOU DOIN MANG");
                    }
                } else {
                    slideshow = !slideshow;
                    // toggle slideshow
                }
            }
        }

        /// <summary>
        /// Read embedded resource
        /// </summary>
        /// <param name="filename">Resource name</param>
        /// <returns>Resource Stream</returns>
        System.IO.Stream embedded(string filename) {
            return System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream("imaeg." + filename);
        }

        /// <summary>
        /// Change to the dir'th picture from this one
        /// </summary>
        /// <param name="dir">Steps, signed</param>
        void nextPic(int dir) {
            cur += dir;
            if (cur >= man.length) cur = 0;
            if (cur < 0) cur = man.length - 1;

            stat.Text = "[!] #" + (cur + 1) + " " + man.getName(cur);
            Application.DoEvents();
            if (dsp.Image != null) {
                dsp.Image.Dispose();
                dsp.Image = null;
            }
            try {
                Image im = man.get(cur);
                //Bug in .NET framework:
                // Viewing GIF files may block image changing
                // until window is minimized/restored.
                // -+- Toggling visibility fixes this.
                //dsp.Visible = false;
                
                // Resizing viewport before showing
                // image results in less flicker
                recenter(im);
                dsp.Image = im;
                //dsp.Visible = true;
                stat.Text = "(" +
                    dsp.Image.Width + "x" +
                    dsp.Image.Height + ") " +
                    stat.Text.Substring(4);

            } catch (Exception ex) {
                dsp.Image = null;
                Application.DoEvents();
                //MessageBox.Show(ex.Source + " -> " +
                //    ex.Message + "\n" + ex.StackTrace);
                stat.Text = "[BAD] " + stat.Text.Substring(4);
            }
        }

        /// <summary>
        /// Removes empty folders on exit
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Form1_FormClosing(object sender, FormClosingEventArgs e) {
            prepare(null);
        }

        /// <summary>
        /// I wonder what this does
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void dsp_MouseDown(object sender, MouseEventArgs e) {
            
            // Set chrome (non-userspace) padding
            chrome = new Point(
                (e.X + dsp.Left) - (Cursor.Position.X - this.Left),
                (e.Y + dsp.Top) - (Cursor.Position.Y - this.Top));

            setZoomOrigin(e.Location);
        }

        /// <summary>
        /// Set zoom pivot by reading viewport location
        /// </summary>
        void setZoomOrigin() {
            setZoomOrigin(getCursorPositionRelativeToWindowPositionThroughTheMathematicalImplementation());
        }

        /// <summary>
        /// Set zoom pivot based on mouseEvent
        /// </summary>
        /// <param name="pt">The clicked absolute coordinate of dsp</param>
        void setZoomOrigin(Point pt) {
            loc = getCursorPositionRelativeToWindowPosition();
            center = loc;
            offX = pt.X / (dsp.Width * 1.0);
            offY = pt.Y / (dsp.Height * 1.0);
            aspect = dsp.Image.Width /
                (1.0 * dsp.Image.Height);
        }

        /// <summary>
        /// Check whether existing viewport is within allowed bounds
        /// Please don't use this method unless you want flickering
        /// </summary>
        /// <returns>The closest valid viewport based on reg</returns>
        Point validateBounds() {
            return validateBounds(dsp.Bounds, true);
        }

        /// <summary>
        /// Validate and (optionally) apply new viewport
        /// </summary>
        /// <param name="reg">The new viewport</param>
        /// <param name="exec">Apply viewport</param>
        /// <returns>The closest valid viewport based on reg</returns>
        Point validateBounds(Rectangle reg, bool exec) {
            int x = reg.Left, y = reg.Top;
            if (lockEdges) {

                // Validate width
                if (reg.Width >= span.Width) {
                    // Larger than screen
                    if (x > 0) x = 0;
                    if (span.Width > reg.Width + x)
                        x = -(reg.Width - span.Width);
                } else {
                    // Smaller than screen
                    if (x < 0) x = 0;
                    if (span.Width < reg.Width + x)
                        x = span.Width - reg.Width;
                }

                // Validate height
                if (reg.Height >= span.Height) {
                    // Larger than screen
                    if (y > 0) y = 0;
                    if (span.Height > reg.Height + y)
                        y = -(reg.Height - span.Height);
                } else {
                    // Smaller than screen
                    if (y < 0) y = 0;
                    if (span.Height < reg.Height + y)
                        y = span.Height - reg.Height;
                }
            }
            //bool ret = (x == reg.Left && y == reg.Top);
            if (exec) dsp.Location = new Point(x, y);
            return new Point(x, y);
        }

        // Relative position reference
        double offX, offY, aspect;
        Point loc = Point.Empty;
        Point center = Point.Empty;

        /// <summary>
        /// Handles mouseMove event (pan/zoom)
        /// </summary>
        /// <param name="sender">Event origin (control)</param>
        /// <param name="e">Event parameters</param>
        bool mouseMoveAvailable = true;
        private void dsp_MouseMove(object sender, MouseEventArgs e) {
            if (!mouseMoveAvailable) return;
            mouseMoveAvailable = false;
            if (e.Button == MouseButtons.Right) {

                // ZOOOOOOOOM
                Point pos = loc;
                loc = getCursorPositionRelativeToWindowPosition();

                // Just to make things less complicated
                int diffX = loc.X - pos.X;
                int diffY = loc.Y - pos.Y;

                // Pythagoras to find length
                double diff = Math.Sqrt(
                    Math.Pow(1.0 * diffX, 2) +
                    Math.Pow(1.0 * diffY, 2));

                // Retain most weighted sign
                if (Math.Abs(diffX) > Math.Abs(diffY))
                    diff *= Math.Sign(diffX);
                else diff *= Math.Sign(diffY);

                zoomImage(diff);

            } else if (e.Button == MouseButtons.Left) {

                // Pan
                Point pos = loc; loc = getCursorPositionRelativeToWindowPosition(); //Cursor.Position;

                // Directly set location
                //dsp.Left += Cursor.Position.X - pos.X;
                //dsp.Top += Cursor.Position.Y - pos.Y;

                // Validate not out of bounds
                validateBounds(new Rectangle(
                    dsp.Left + (loc.X - pos.X),
                    dsp.Top + (loc.Y - pos.Y),
                    dsp.Width, dsp.Height), true);
            }
            mouseMoveAvailable = true;
        }

        /// <summary>
        /// Feeling real ENTERPRISE
        /// </summary>
        /// <returns>The Cursor Position Relative To Window Position (Thus Not The Absolute Position Of The Cursor)</returns>
        Point getCursorPositionRelativeToWindowPosition() {
            Point ret = Cursor.Position;
            ret.X -= this.Left - chrome.X;
            ret.Y -= this.Top - chrome.Y;
            return ret;
        }

        /// <summary>
        /// Alternative vershun
        /// </summary>
        /// <returns>Mathematical variation of gCPRTWP; more suitable for zoomfix</returns>
        Point getCursorPositionRelativeToWindowPositionThroughTheMathematicalImplementation() {
            return new Point(
                Cursor.Position.X - dsp.Left - this.Left,
                Cursor.Position.Y - dsp.Top - this.Top);
        }

        /// <summary>
        /// Zooms the image lambda pizels
        /// </summary>
        /// <param name="lambda">Amount of pixels to append</param>
        void zoomImage(double lambda) {

            // I'm too tired to figure out why this is
            // necessary right now, I'm just happy it works
            if (lambda < 0) lambda /= 1.6;

            // Slightly higher performance
            double w = dsp.Width; //, h = dsp.Height;

            // Calculate new size and offset
            double newW = Math.Max(25,
                w + w * 0.005 * lambda);
            double newH = newW / aspect;
            double newOffX = -(newW * offX) + center.X;
            double newOffY = -(newH * offY) + center.Y;

            // Apply the change
            if (newW * newH < 104857600) { //10240^2
                dsp.Width = (int)newW;
                dsp.Height = (int)newH;

                // Old pivot method; centered on screen
                //dsp.Left -= (int)(dsp.Width - w) / 2;
                //dsp.Top -= (int)(dsp.Height - h) / 2;

                // New pivot method; weighted on original rightclick coordinate
                dsp.Left = (int)newOffX;
                dsp.Top = (int)newOffY;
                Application.DoEvents();
            }
        }

        /// <summary>
        /// Smoothly scroll the image to given position
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        void scrollSmooth(int x, int y) {
            
            // Filter repetitious scrolls
            scrollIdent++;
            int myIdent = scrollIdent;

            // Make sure we're within bounds
            Point xy = validateBounds(new Rectangle(x, y, dsp.Width, dsp.Height), false);
            x = xy.X; y = xy.Y;         // expand validated position
            
            int xa = dsp.Left;          // read current ("from") pos
            int ya = dsp.Top;
            int xd = x - xa;            // difference between cur and target
            int yd = y - ya;
            int xs = Math.Sign(xd);     // get direction
            int ys = Math.Sign(yd);
            xd *= xs;                   // filter direction from distance
            yd *= ys;
            Size sz = dsp.Size;
            int[] stepsize = { 200, 100, 50, 25, 12, 6, 3, 2, 1 };
            while (xd > 1 || yd > 1) {
                bool xb = false, yb = false;            // stepped this axis yet?
                foreach (int step in stepsize) {        // try all stepsizes
                    Point newpos = new Point(xa, ya);   // original position
                    if (!xb && step < xd) {             // can & should step X axis?
                        xd -= step;                     // subtract steps from dist
                        xa += step * (1 * xs);          // add step to position
                        xb = true;                      // stepped X axis
                    }
                    if (!yb && step < yd) {
                        yd -= step;
                        ya += step * (1 * ys);
                        yb = true;
                    }
                    /*if (step < dist) {
                        dist -= step;
                        from += step * (-1 * sign);
                        if (!validateBounds(new Rectangle(dsp.Left, from, dsp.Width, dsp.Height))) dist = 0;
                        Application.DoEvents();
                        System.Threading.Thread.Sleep(50);
                        break;
                    }*/
                }
                if (myIdent != scrollIdent) break;
                validateBounds(new Rectangle(xa, ya, sz.Width, sz.Height), true);
                Application.DoEvents();
                System.Threading.Thread.Sleep(20);
            }
        }

        /// <summary>
        /// Apply and center a best-fit viewport
        /// </summary>
        void recenter() {
            if (dsp.Image != null) recenter(dsp.Image);
        }

        /// <summary>
        /// Apply and center a best-fit viewport based on supplied bitmap
        /// </summary>
        /// <param name="bm">Image to base calculations on</param>
        void recenter(Image bm) {
            int durrX, durrY;
            double derpX = bm.Width / (1.0 * span.Width);
            double derpY = bm.Height / (1.0 * span.Height);
            if (derpX > derpY) {
                durrX = span.Width;
                durrY = (int)(bm.Height / derpX);
                dsp.Location = new Point(0, (span.Height - durrY) / 2);
            } else {
                durrY = span.Height;
                durrX = (int)(bm.Width / derpY);
                dsp.Location = new Point((span.Width - durrX) / 2, 0);
            }
            dsp.Size = new Size(durrX, durrY);
        }

        /// <summary>
        /// Hurr
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Form1_Resize(object sender, EventArgs e) {
            recenter(); //NO DOCKING ALLOWED
        }

        /// <summary>
        /// Durr
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void dsp_MouseWheel(object sender, MouseEventArgs e) {
            setZoomOrigin();
            zoomImage(e.Delta);
        }
    }
}